//    OpenVPN -- An application to securely tunnel IP networks
//               over a single port, with support for SSL/TLS-based
//               session authentication and key exchange,
//               packet encryption, packet authentication, and
//               packet compression.
//
//    Copyright (C) 2012-2022 OpenVPN Inc.
//
//    This program is free software: you can redistribute it and/or modify
//    it under the terms of the GNU Affero General Public License Version 3
//    as published by the Free Software Foundation.
//
//    This program is distributed in the hope that it will be useful,
//    but WITHOUT ANY WARRANTY; without even the implied warranty of
//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//    GNU Affero General Public License for more details.
//
//    You should have received a copy of the GNU Affero General Public License
//    along with this program in the COPYING file.
//    If not, see <http://www.gnu.org/licenses/>.

// HTTP proxy transport object.

#ifndef OPENVPN_TRANSPORT_CLIENT_HTTPCLI_H
#define OPENVPN_TRANSPORT_CLIENT_HTTPCLI_H

#include <vector>
#include <string>
#include <sstream>
#include <algorithm> // for std::min
#include <memory>

#include <openvpn/io/io.hpp>

#include <openvpn/common/size.hpp>
#include <openvpn/common/exception.hpp>
#include <openvpn/common/string.hpp>
#include <openvpn/common/base64.hpp>
#include <openvpn/common/split.hpp>
#include <openvpn/common/options.hpp>
#include <openvpn/common/number.hpp>
#include <openvpn/common/userpass.hpp>
#include <openvpn/buffer/bufstr.hpp>
#include <openvpn/buffer/buflimit.hpp>
#include <openvpn/transport/tcplink.hpp>
#include <openvpn/transport/client/transbase.hpp>
#include <openvpn/transport/socket_protect.hpp>
#include <openvpn/transport/protocol.hpp>
#include <openvpn/http/reply.hpp>
#include <openvpn/http/status.hpp>
#include <openvpn/http/htmlskip.hpp>
#include <openvpn/proxy/proxyauth.hpp>
#include <openvpn/proxy/httpdigest.hpp>
#include <openvpn/proxy/ntlm.hpp>
#include <openvpn/client/remotelist.hpp>
#include <openvpn/crypto/digestapi.hpp>

namespace openvpn {
namespace HTTPProxyTransport {

enum AuthMethod
{
    None,
    Basic,
    Digest,
    Ntlm,
    Any
};

class Options : public RC<thread_safe_refcount>
{
  public:
    struct CustomHeader : public RC<thread_unsafe_refcount>
    {
        typedef RCPtr<CustomHeader> Ptr;

        std::string p1;
        std::string p2;
    };

    struct CustomHeaderList : public std::vector<CustomHeader::Ptr>
    {
    };

    typedef RCPtr<Options> Ptr;

    RemoteList::Ptr proxy_server;
    std::string username;
    std::string password;
    AuthMethod auth_method = Any;
    bool allow_cleartext_auth = false;

    std::string http_version;
    std::string user_agent;

    CustomHeaderList headers;

    void set_proxy_server(const std::string &host, const std::string &port)
    {
        proxy_server.reset(new RemoteList(host, port, Protocol(Protocol::TCP), "http proxy port"));
    }

    void proxy_server_set_enable_cache(const bool enable_cache)
    {
        proxy_server->set_enable_cache(enable_cache);
    }

    void proxy_server_precache(RemoteList::Ptr &r)
    {
        if (proxy_server->get_enable_cache())
            r = proxy_server;
    }

    static Ptr parse(const OptionList &opt)
    {
        if (opt.exists("http-proxy"))
        {
            Ptr obj(new Options);
            if (obj->parse_options(opt))
                return obj;
        }
        return Ptr();
    }

  private:
    bool parse_options(const OptionList &opt)
    {
        const Option *hp = opt.get_ptr("http-proxy");
        if (hp)
        {
            // get server/port
            set_proxy_server(hp->get(1, 256), hp->get(2, 16));

            // get creds
            UserPass::parse(opt, "http-proxy-user-pass", 0, username, password);

            const std::string auth = hp->get_optional(3, 16);
            if (!auth.empty())
            {
                if (auth == "auto")
                {
                    allow_cleartext_auth = true;
                    auth_method = Any;
                }
                else if (auth == "auto-nct")
                {
                    allow_cleartext_auth = false;
                    auth_method = Any;
                }
                else if (auth == "basic")
                {
                    allow_cleartext_auth = true;
                    auth_method = Basic;
                }
                else if (auth == "digest")
                {
                    allow_cleartext_auth = false;
                    auth_method = Digest;
                }
                else if (auth == "ntlm")
                {
                    allow_cleartext_auth = false;
                    auth_method = Ntlm;
                }
                else if (auth == "none")
                {
                    auth_method = None;
                }
                else
                {
                    throw Exception("Unsupported HTTP proxy auth method: " + auth);
                }
            }

            // get options
            const OptionList::IndexList *hpo = opt.get_index_ptr("http-proxy-option");
            if (hpo)
            {
                for (OptionList::IndexList::const_iterator i = hpo->begin(); i != hpo->end(); ++i)
                {
                    const Option &o = opt[*i];
                    const std::string &type = o.get(1, 64);
                    if (type == "VERSION")
                    {
                        http_version = o.get(2, 16);
                        o.touch();
                    }
                    else if (type == "AGENT")
                    {
                        user_agent = o.get(2, 256);
                        o.touch();
                    }
                    else if (type == "EXT1" || type == "EXT2" || type == "CUSTOM-HEADER")
                    {
                        CustomHeader::Ptr h(new CustomHeader());
                        h->p1 = o.get(2, 512);
                        h->p2 = o.get_optional(3, 512);
                        headers.push_back(h);
                        o.touch();
                    }
                }
            }
            return true;
        }
        else
            return false;
    }
};

class ClientConfig : public TransportClientFactory
{
  public:
    typedef RCPtr<ClientConfig> Ptr;

    RemoteList::Ptr remote_list;
    size_t free_list_max_size;
    Frame::Ptr frame;
    SessionStats::Ptr stats;

    Options::Ptr http_proxy_options;

    StrongRandomAPI::Ptr rng; // random data source

    DigestFactory::Ptr digest_factory; // needed by proxy auth methods

    SocketProtect *socket_protect;

    bool skip_html;

    static Ptr new_obj()
    {
        return new ClientConfig;
    }

    virtual TransportClient::Ptr new_transport_client_obj(openvpn_io::io_context &io_context,
                                                          TransportClientParent *parent);

  private:
    ClientConfig()
        : free_list_max_size(8),
          socket_protect(nullptr),
          skip_html(false)
    {
    }
};

class Client : public TransportClient, AsyncResolvableTCP
{
    typedef RCPtr<Client> Ptr;

    typedef TCPTransport::TCPLink<openvpn_io::ip::tcp, Client *, false> LinkImpl;

    friend class ClientConfig; // calls constructor
    friend LinkImpl::Base;     // calls tcp_read_handler

  public:
    void transport_start() override
    {
        if (!impl)
        {
            if (!config->http_proxy_options)
            {
                parent->proxy_error(Error::PROXY_ERROR, "http_proxy_options not defined");
                return;
            }

            halt = false;

            // Get target server host:port.  We don't care about resolving it
            // since proxy server will do that for us.
            remote_list().endpoint_available(&server_host, &server_port, nullptr);

            // Get proxy server host:port, and resolve it if not already cached
            if (proxy_remote_list().endpoint_available(&proxy_host, &proxy_port, nullptr))
            {
                // already cached
                start_connect_();
            }
            else
            {
                // resolve it
                parent->transport_pre_resolve();

                async_resolve_lock();
                async_resolve_name(proxy_host, proxy_port);
            }
        }
    }

    bool transport_send_const(const Buffer &buf) override
    {
        return send_const(buf);
    }

    bool transport_send(BufferAllocated &buf) override
    {
        return send(buf);
    }

    bool transport_send_queue_empty() override
    {
        if (impl)
            return impl->send_queue_empty();
        else
            return false;
    }

    bool transport_has_send_queue() override
    {
        return true;
    }

    void transport_stop_requeueing() override
    {
    }

    size_t transport_send_queue_size() override
    {
        if (impl)
            return impl->send_queue_size();
        else
            return 0;
    }

    void reset_align_adjust(const size_t align_adjust) override
    {
        if (impl)
            impl->reset_align_adjust(align_adjust);
    }

    void server_endpoint_info(std::string &host, std::string &port, std::string &proto, std::string &ip_addr) const override
    {
        host = server_host;
        port = server_port;
        const IP::Addr addr = server_endpoint_addr();
        proto = "TCP";
        proto += addr.version_string();
        proto += "-via-HTTP";
        ip_addr = addr.to_string();
    }

    IP::Addr server_endpoint_addr() const override
    {
        return IP::Addr::from_asio(server_endpoint.address());
    }

    Protocol transport_protocol() const override
    {
        if (server_endpoint.address().is_v4())
            return Protocol(Protocol::TCPv4);
        else if (server_endpoint.address().is_v6())
            return Protocol(Protocol::TCPv6);
        else
            return Protocol();
    }

    void stop() override
    {
        stop_();
    }
    virtual ~Client()
    {
        stop_();
    }

  private:
    struct ProxyResponseLimit : public BufferLimit<size_t>
    {
        ProxyResponseLimit()
            : BufferLimit(1024, 65536)
        {
        }

        virtual void bytes_exceeded()
        {
            OPENVPN_THROW_EXCEPTION("HTTP proxy response too large (> " << max_bytes << " bytes)");
        }

        virtual void lines_exceeded()
        {
            OPENVPN_THROW_EXCEPTION("HTTP proxy response too large (> " << max_lines << " lines)");
        }
    };

    Client(openvpn_io::io_context &io_context_arg,
           ClientConfig *config_arg,
           TransportClientParent *parent_arg)
        : AsyncResolvableTCP(io_context_arg),
          socket(io_context_arg),
          config(config_arg),
          parent(parent_arg),
          halt(false),
          n_transactions(0),
          proxy_established(false),
          http_reply_status(HTTP::ReplyParser::pending),
          ntlm_phase_2_response_pending(false),
          drain_content_length(0)
    {
    }

    void transport_reparent(TransportClientParent *parent_arg) override
    {
        parent = parent_arg;
    }

    bool send_const(const Buffer &cbuf)
    {
        if (impl)
        {
            BufferAllocated buf(cbuf, 0);
            return impl->send(buf);
        }
        else
            return false;
    }

    bool send(BufferAllocated &buf)
    {
        if (impl)
            return impl->send(buf);
        else
            return false;
    }

    void tcp_error_handler(const char *error) // called by LinkImpl and internally
    {
        std::ostringstream os;
        os << "Transport error on '" << server_host << "' via HTTP proxy " << proxy_host << ':' << proxy_port << " : " << error;
        stop();
        parent->transport_error(Error::TRANSPORT_ERROR, os.str());
    }

    void proxy_error(const Error::Type fatal_err, const std::string &what)
    {
        std::ostringstream os;
        os << "on " << proxy_host << ':' << proxy_port << ": " << what;
        stop();
        parent->proxy_error(fatal_err, os.str());
    }

    bool tcp_read_handler(BufferAllocated &buf) // called by LinkImpl
    {
        if (proxy_established)
        {
            if (!html_skip)
                parent->transport_recv(buf);
            else
                drain_html(buf); // skip extraneous HTML after header
        }
        else
        {
            try
            {
                proxy_read_handler(buf);
            }
            catch (const std::exception &e)
            {
                proxy_error(Error::PROXY_ERROR, e.what());
            }
        }
        return true;
    }

    void tcp_write_queue_needs_send() // called by LinkImpl
    {
        if (proxy_established)
            parent->transport_needs_send();
    }

    void tcp_eof_handler() // called by LinkImpl
    {
        if (proxy_established)
        {
            config->stats->error(Error::NETWORK_EOF_ERROR);
            tcp_error_handler("NETWORK_EOF_ERROR");
        }
        else
        {
            try
            {
                proxy_eof_handler();
            }
            catch (const std::exception &e)
            {
                proxy_error(Error::PROXY_ERROR, e.what());
            }
        }
    }

    void proxy_read_handler(BufferAllocated &buf)
    {
        // for anti-DoS, only allow a maximum number of chars in HTTP response
        proxy_response_limit.add(buf);

        if (http_reply_status == HTTP::ReplyParser::pending)
        {
            OPENVPN_LOG_NTNL("FROM PROXY: " << buf_to_string(buf));
            for (size_t i = 0; i < buf.size(); ++i)
            {
                http_reply_status = http_parser.consume(http_reply, (char)buf[i]);
                if (http_reply_status != HTTP::ReplyParser::pending)
                {
                    buf.advance(i + 1);
                    if (http_reply_status == HTTP::ReplyParser::success)
                    {
                        // OPENVPN_LOG("*** HTTP header parse complete, resid_size=" << buf.size());
                        // OPENVPN_LOG(http_reply.to_string());

                        // we are connected, switch socket to tunnel mode
                        if (http_reply.status_code == HTTP::Status::Connected)
                        {
                            if (config->skip_html)
                            {
                                proxy_half_connected();
                                html_skip.reset(new HTTP::HTMLSkip());
                                drain_html(buf);
                            }
                            else
                                proxy_connected(buf, true);
                        }
                        else if (ntlm_phase_2_response_pending)
                            ntlm_auth_phase_2_pre();
                    }
                    else
                    {
                        throw Exception("HTTP proxy header parse error");
                    }
                    break;
                }
            }
        }

        // handle draining of content controlled by Content-length header
        if (drain_content_length)
        {
            const size_t drain = std::min(drain_content_length, buf.size());
            buf.advance(drain);
            drain_content_length -= drain;
            if (!drain_content_length)
            {
                if (ntlm_phase_2_response_pending)
                    ntlm_auth_phase_2();
            }
        }
    }

    void proxy_connected(BufferAllocated &buf, const bool notify_parent)
    {
        proxy_established = true;
        if (parent->transport_is_openvpn_protocol())
        {
            // switch socket from HTTP proxy handshake mode to OpenVPN protocol mode
            impl->set_raw_mode(false);
            if (notify_parent)
                parent->transport_connecting();
            try
            {
                impl->inject(buf);
            }
            catch (const std::exception &e)
            {
                proxy_error(Error::PROXY_ERROR, std::string("post-header inject error: ") + e.what());
                return;
            }
        }
        else
        {
            if (notify_parent)
                parent->transport_connecting();
            parent->transport_recv(buf);
        }
    }

    // Called after header received but before possible extraneous HTML
    // is drained.  At this point, we are in a state where output data
    // (if OpenVPN protocol) is packetized, but input data is still in
    // raw mode as we search the input stream for the end of the
    // extraneous HTML.  When we reach the beginning of payload data,
    // proxy_connected() should be called with notify_parent == false.
    void proxy_half_connected()
    {
        proxy_established = true;
        if (parent->transport_is_openvpn_protocol())
            impl->set_raw_mode_write(false);
        parent->transport_connecting();
    }

    void drain_html(BufferAllocated &buf)
    {
        while (!buf.empty())
        {
            switch (html_skip->add(buf.pop_front()))
            {
            case HTTP::HTMLSkip::MATCH:
            case HTTP::HTMLSkip::NOMATCH:
                {
                    OPENVPN_LOG("Proxy: Skipped " << html_skip->n_bytes() << " byte(s) of HTML");
                    html_skip->get_residual(buf);
                    html_skip.reset();
                    proxy_connected(buf, false);
                    return;
                }
            case HTTP::HTMLSkip::PENDING:
                break;
            }
        }
    }

    HTTPProxy::ProxyAuthenticate::Ptr get_proxy_authenticate_header(const char *type)
    {
        for (HTTP::HeaderList::const_iterator i = http_reply.headers.begin(); i != http_reply.headers.end(); ++i)
        {
            const HTTP::Header &h = *i;
            if (string::strcasecmp(h.name, "proxy-authenticate") == 0)
            {
                HTTPProxy::ProxyAuthenticate::Ptr pa = new HTTPProxy::ProxyAuthenticate(h.value);
                if (string::strcasecmp(type, pa->method) == 0)
                    return pa;
            }
        }
        return HTTPProxy::ProxyAuthenticate::Ptr();
    }

    void proxy_eof_handler()
    {
        if (http_reply_status == HTTP::ReplyParser::success)
        {
            if (http_reply.status_code == HTTP::Status::ProxyAuthenticationRequired)
            {
                if (config->http_proxy_options->auth_method == None)
                    throw Exception("HTTP proxy authentication is disabled");
                if (n_transactions > 1)
                {
                    proxy_error(Error::PROXY_NEED_CREDS, "HTTP proxy credentials were not accepted");
                    return;
                }
                if (config->http_proxy_options->username.empty())
                {
                    proxy_error(Error::PROXY_NEED_CREDS, "HTTP proxy requires credentials");
                    return;
                }

                HTTPProxy::ProxyAuthenticate::Ptr pa;
                const AuthMethod method(config->http_proxy_options->auth_method);

                if (method == Any || method == Ntlm)
                {
                    pa = get_proxy_authenticate_header("ntlm");
                    if (pa)
                    {
                        ntlm_auth_phase_1(*pa);
                        return;
                    }
                }

                if (method == Any || method == Digest)
                {
                    pa = get_proxy_authenticate_header("digest");
                    if (pa)
                    {
                        digest_auth(*pa);
                        return;
                    }
                }

                if (method == Any || method == Basic)
                {
                    pa = get_proxy_authenticate_header("basic");
                    if (pa)
                    {
                        if (!config->http_proxy_options->allow_cleartext_auth)
                            throw Exception("HTTP proxy basic authentication not allowed by user preference");

                        basic_auth(*pa);
                        return;
                    }
                }

                throw Exception("HTTP proxy-authenticate method not allowed / supported");
            }
            else if (http_reply.status_code == HTTP::Status::ProxyError
                     || http_reply.status_code == HTTP::Status::NotFound
                     || http_reply.status_code == HTTP::Status::ServiceUnavailable)
            {
                // this is a nonfatal error, so we pass Error::UNDEF to tell the upper layer to
                // retry the connection
                proxy_error(Error::UNDEF, "HTTP proxy server could not connect to OpenVPN server");
                return;
            }
            else if (http_reply.status_code == HTTP::Status::Forbidden)
                OPENVPN_THROW_EXCEPTION("HTTP proxy returned Forbidden status code");
            else
                OPENVPN_THROW_EXCEPTION("HTTP proxy status code: " << http_reply.status_code);
        }
        else if (http_reply_status == HTTP::ReplyParser::pending)
            throw Exception("HTTP proxy unexpected EOF: reply incomplete");
        else
            throw Exception("HTTP proxy general error");
    }

    void basic_auth(HTTPProxy::ProxyAuthenticate &pa)
    {
        OPENVPN_LOG("Proxy method: Basic" << std::endl
                                          << pa.to_string());

        std::ostringstream os;
        gen_headers(os);
        os << "Proxy-Authorization: Basic "
           << base64->encode(config->http_proxy_options->username + ':' + config->http_proxy_options->password)
           << "\r\n";
        http_request = os.str();
        reset();
        start_connect_();
    }

    void digest_auth(HTTPProxy::ProxyAuthenticate &pa)
    {
        try
        {
            OPENVPN_LOG("Proxy method: Digest" << std::endl
                                               << pa.to_string());

            // constants
            const std::string http_method = "CONNECT";
            const std::string nonce_count = "00000001";
            const std::string qop = "auth";

            // get values from Proxy-Authenticate header
            const std::string realm = pa.parms.get_value("realm");
            const std::string nonce = pa.parms.get_value("nonce");
            const std::string algorithm = pa.parms.get_value("algorithm");
            const std::string opaque = pa.parms.get_value("opaque");

            // generate a client nonce
            unsigned char cnonce_raw[8];
            config->rng->rand_bytes(cnonce_raw, sizeof(cnonce_raw));
            const std::string cnonce = render_hex(cnonce_raw, sizeof(cnonce_raw));

            // build URI
            const std::string uri = server_host + ":" + server_port;

            // calculate session key
            const std::string session_key = HTTPProxy::Digest::calcHA1(
                *config->digest_factory,
                algorithm,
                config->http_proxy_options->username,
                realm,
                config->http_proxy_options->password,
                nonce,
                cnonce);

            // calculate response
            const std::string response = HTTPProxy::Digest::calcResponse(
                *config->digest_factory,
                session_key,
                nonce,
                nonce_count,
                cnonce,
                qop,
                http_method,
                uri,
                "");

            // generate proxy request
            std::ostringstream os;
            gen_headers(os);
            os << "Proxy-Authorization: Digest username=\"" << config->http_proxy_options->username << "\", realm=\"" << realm << "\", nonce=\"" << nonce << "\", uri=\"" << uri << "\", qop=" << qop << ", nc=" << nonce_count << ", cnonce=\"" << cnonce << "\", response=\"" << response << "\"";
            if (!opaque.empty())
                os << ", opaque=\"" + opaque + "\"";
            os << "\r\n";

            http_request = os.str();
            reset();
            start_connect_();
        }
        catch (const std::exception &e)
        {
            proxy_error(Error::PROXY_ERROR, std::string("Digest Auth: ") + e.what());
        }
    }

    std::string get_ntlm_phase_2_response()
    {
        for (HTTP::HeaderList::const_iterator i = http_reply.headers.begin(); i != http_reply.headers.end(); ++i)
        {
            const HTTP::Header &h = *i;
            if (string::strcasecmp(h.name, "proxy-authenticate") == 0)
            {
                std::vector<std::string> v = Split::by_space<std::vector<std::string>, StandardLex, SpaceMatch, Split::NullLimit>(h.value);
                if (v.size() >= 2 && string::strcasecmp("ntlm", v[0]) == 0)
                    return v[1];
            }
        }
        return "";
    }

    void ntlm_auth_phase_1(HTTPProxy::ProxyAuthenticate &pa)
    {
        OPENVPN_LOG("Proxy method: NTLM" << std::endl
                                         << pa.to_string());

        const std::string phase_1_reply = HTTPProxy::NTLM::phase_1();

        std::ostringstream os;
        gen_headers(os);
        os << "Proxy-Connection: Keep-Alive\r\n";
        os << "Proxy-Authorization: NTLM " << phase_1_reply << "\r\n";

        http_request = os.str();
        reset();
        ntlm_phase_2_response_pending = true;
        start_connect_();
    }

    void ntlm_auth_phase_2_pre()
    {
        // if content exists, drain it first, then progress to ntlm_auth_phase_2
        const std::string content_length_str = http_reply.headers.get_value_trim("content-length");
        const unsigned int content_length = parse_number_throw<unsigned int>(content_length_str, "content-length");
        if (content_length)
            drain_content_length = content_length;
        else
            ntlm_auth_phase_2();
    }

    void ntlm_auth_phase_2()
    {
        ntlm_phase_2_response_pending = false;

        if (http_reply.status_code != HTTP::Status::ProxyAuthenticationRequired)
            throw Exception("NTLM phase-2 status is not ProxyAuthenticationRequired");

        const std::string phase_2_response = get_ntlm_phase_2_response();
        if (!phase_2_response.empty())
            ntlm_auth_phase_3(phase_2_response);
        else
            throw Exception("NTLM phase-2 response missing");
    }

    void ntlm_auth_phase_3(const std::string &phase_2_response)
    {
        // do the NTLMv2 handshake
        try
        {
            // OPENVPN_LOG("NTLM phase 3: " << phase_2_response);

            const std::string phase_3_reply = HTTPProxy::NTLM::phase_3(
                *config->digest_factory,
                phase_2_response,
                config->http_proxy_options->username,
                config->http_proxy_options->password,
                *config->rng);

            std::ostringstream os;
            gen_headers(os);
            os << "Proxy-Connection: Keep-Alive\r\n";
            os << "Proxy-Authorization: NTLM " << phase_3_reply << "\r\n";

            http_request = os.str();
            reset_partial();
            http_proxy_send();
        }
        catch (const std::exception &e)
        {
            std::string what{e.what()};
            if (what.find("openssl_digest_error") != std::string::npos)
            {
                proxy_error(Error::NTLM_MISSING_CRYPTO, "Crypto primitives required for NTLM authentication are unavailable");
            }
            else
            {
                proxy_error(Error::PROXY_ERROR, std::string("NTLM Auth: ") + e.what());
            }
        }
    }

    void gen_headers(std::ostringstream &os)
    {
        bool host_header_sent = false;

        // emit custom headers
        {
            const Options::CustomHeaderList &headers = config->http_proxy_options->headers;
            for (Options::CustomHeaderList::const_iterator i = headers.begin(); i != headers.end(); ++i)
            {
                const Options::CustomHeader &h = **i;
                if (!h.p2.empty())
                {
                    os << h.p1 << ": " << h.p2 << "\r\n";
                    if (!string::strcasecmp(h.p1, "host"))
                        host_header_sent = true;
                }
                else
                {
                    os << h.p1 << "\r\n";
                    const std::string h5 = h.p1.substr(0, 5);
                    if (!string::strcasecmp(h5, "host:"))
                        host_header_sent = true;
                }
            }
        }

        // emit user-agent header
        {
            const std::string &user_agent = config->http_proxy_options->user_agent;
            if (!user_agent.empty())
                os << "User-Agent: " << user_agent << "\r\n";
        }

        // emit host header
        if (!host_header_sent)
            os << "Host: " << server_host << "\r\n";
    }

    void stop_()
    {
        if (!halt)
        {
            halt = true;
            if (impl)
                impl->stop();

            socket.close();
            async_resolve_cancel();
        }
    }

    // do DNS resolve
    void resolve_callback(const openvpn_io::error_code &error,
                          results_type results) override
    {
        // release resolver allocated resources
        async_resolve_cancel();

        if (!halt)
        {
            if (!error)
            {
                // save resolved endpoint list in proxy remote_list
                proxy_remote_list().set_endpoint_range(results);
                start_connect_();
            }
            else
            {
                std::ostringstream os;
                os << "DNS resolve error on '" << proxy_host << "' for TCP (HTTP proxy): " << error.message();
                config->stats->error(Error::RESOLVE_ERROR);
                stop();
                parent->transport_error(Error::UNDEF, os.str());
            }
        }
    }

    void reset()
    {
        stop();
        halt = false;
        proxy_response_limit.reset();
        proxy_established = false;
        reset_partial();
    }

    void reset_partial()
    {
        http_reply_status = HTTP::ReplyParser::pending;
        http_reply.reset();
        http_parser.reset();
        ntlm_phase_2_response_pending = false;
        drain_content_length = 0;
        html_skip.reset();
    }

    // do TCP connect
    void start_connect_()
    {
        proxy_remote_list().get_endpoint(server_endpoint);
        OPENVPN_LOG("Contacting " << server_endpoint << " via HTTP Proxy");
        parent->transport_wait_proxy();
        socket.open(server_endpoint.protocol());

        if (config->socket_protect)
        {
            if (!config->socket_protect->socket_protect(socket.native_handle(), server_endpoint_addr()))
            {
                config->stats->error(Error::SOCKET_PROTECT_ERROR);
                stop();
                parent->transport_error(Error::UNDEF, "socket_protect error (HTTP Proxy)");
                return;
            }
        }

        socket.set_option(openvpn_io::ip::tcp::no_delay(true));
        socket.async_connect(server_endpoint, [self = Ptr(this)](const openvpn_io::error_code &error)
                             {
                                                OPENVPN_ASYNC_HANDLER;
                                                self->start_impl_(error); });
    }

    // start I/O on TCP socket
    void start_impl_(const openvpn_io::error_code &error)
    {
        if (!halt)
        {
            if (!error)
            {
                parent->transport_wait();
                impl.reset(new LinkImpl(this,
                                        socket,
                                        0, // send_queue_max_size is unlimited because we regulate size in cliproto.hpp
                                        config->free_list_max_size,
                                        (*config->frame)[Frame::READ_LINK_TCP],
                                        config->stats));
                impl->set_raw_mode(true);
                impl->start();
                ++n_transactions;

                // tell proxy to connect through to OpenVPN server
                http_proxy_send();
            }
            else
            {
                proxy_remote_list().next();

                std::ostringstream os;
                os << "TCP connect error on '" << proxy_host << ':' << proxy_port << "' (" << server_endpoint << ") for TCP-via-HTTP-proxy session: " << error.message();
                config->stats->error(Error::TCP_CONNECT_ERROR);
                stop();
                parent->transport_error(Error::UNDEF, os.str());
            }
        }
    }

    void http_proxy_send()
    {
        BufferAllocated buf;
        create_http_connect_msg(buf);
        send(buf);
    }

    // create HTTP CONNECT message
    void create_http_connect_msg(BufferAllocated &buf)
    {
        std::ostringstream os;
        const std::string &http_version = config->http_proxy_options->http_version;
        os << "CONNECT " << server_host << ':' << server_port << " HTTP/";
        if (!http_version.empty())
            os << http_version;
        else
            os << "1.0";
        os << "\r\n";
        if (!http_request.empty())
            os << http_request;
        else
            gen_headers(os);
        os << "\r\n";
        const std::string str = os.str();
        http_request = "";

        OPENVPN_LOG_NTNL("TO PROXY: " << str);

        config->frame->prepare(Frame::WRITE_HTTP, buf);
        buf_write_string(buf, str);
    }

    RemoteList &remote_list() const
    {
        return *config->remote_list;
    }
    RemoteList &proxy_remote_list() const
    {
        return *config->http_proxy_options->proxy_server;
    }

    std::string proxy_host;
    std::string proxy_port;

    std::string server_host;
    std::string server_port;

    openvpn_io::ip::tcp::socket socket;
    ClientConfig::Ptr config;
    TransportClientParent *parent;
    LinkImpl::Ptr impl;
    LinkImpl::protocol::endpoint server_endpoint;
    bool halt;

    unsigned int n_transactions;
    ProxyResponseLimit proxy_response_limit;
    bool proxy_established;
    HTTP::ReplyParser::status http_reply_status;
    HTTP::Reply http_reply;
    HTTP::ReplyParser http_parser;
    std::string http_request;

    bool ntlm_phase_2_response_pending;
    size_t drain_content_length;

    std::unique_ptr<HTTP::HTMLSkip> html_skip;
};

inline TransportClient::Ptr ClientConfig::new_transport_client_obj(openvpn_io::io_context &io_context, TransportClientParent *parent)
{
    return TransportClient::Ptr(new Client(io_context, this, parent));
}
} // namespace HTTPProxyTransport
} // namespace openvpn

#endif
